<script lang="tsx">
import type { Key } from 'ant-design-vue/es/vc-tree/interface';
import type { CheckInfo } from 'ant-design-vue/es/vc-tree/props';

import type { CSSProperties } from 'vue';

import type { Recordable } from '@vben/types';

import type {
  CheckKeys,
  FieldNames,
  TreeActionItem,
  TreeActionType,
  TreeItem,
  TreeState,
} from './types';

import type { CreateContextOptions } from '#/components/context-menu';

import {
  computed,
  defineComponent,
  onMounted,
  reactive,
  ref,
  toRaw,
  unref,
  watch,
  watchEffect,
} from 'vue';

import { useAccess } from '@vben/access';
import { useNamespace } from '@vben/hooks';

import { Empty, Spin, Tree } from 'ant-design-vue';
import {
  cloneDeep,
  difference,
  get,
  isArray,
  isBoolean,
  isEmpty,
  isFunction,
  omit,
} from 'lodash-es';

import { ScrollContainer } from '#/components/container';
import { Icon } from '#/components/icon';
import { useContextMenu } from '#/hooks/web/useContextMenu';
import { eachTree, filter, treeToList } from '#/utils/helper/treeHelper';
import { extendSlots, getSlot } from '#/utils/helper/tsxHelper';

import TreeHeader from './components/tree-header.vue';
import { useTree } from './hooks/use-tree';
import { treeEmits, treeProps } from './types';

export default defineComponent({
  name: 'BasicTree',
  components: { Tree, TreeHeader, ScrollContainer, Empty, Spin },
  inheritAttrs: false,
  props: treeProps,
  emits: treeEmits,
  setup(props, { attrs, slots, emit, expose }) {
    const { b, e } = useNamespace('tree');
    const state = reactive<TreeState>({
      checkStrictly: props.checkStrictly,
      expandedKeys: props.expandedKeys || [],
      selectedKeys: props.selectedKeys || [],
      checkedKeys: props.checkedKeys || [],
    });

    const searchState = reactive({
      startSearch: false,
      searchText: '',
      searchData: [] as TreeItem[],
    });

    const treeDataRef = ref<TreeItem[]>([]);

    const [createContextMenu] = useContextMenu();

    const getFieldNames = computed((): Required<FieldNames> => {
      const { fieldNames } = props;
      return {
        children: 'children',
        title: 'title',
        key: 'key',
        ...fieldNames,
      };
    });

    const getBindValues = computed(() => {
      const propsData = {
        blockNode: true,
        ...attrs,
        ...props,
        expandedKeys: state.expandedKeys,
        selectedKeys: state.selectedKeys,
        checkedKeys: state.checkedKeys,
        checkStrictly: state.checkStrictly,
        fieldNames: unref(getFieldNames),
        'onUpdate:expandedKeys': (v: Key[]) => {
          state.expandedKeys = v;
          emit('update:expandedKeys', v);
        },
        'onUpdate:selectedKeys': (v: Key[]) => {
          state.selectedKeys = v;
          emit('update:selectedKeys', v);
        },
        onCheck: (v: CheckKeys, e: CheckInfo) => {
          let currentValue = toRaw(state.checkedKeys) as Key[];
          if (isArray(currentValue) && searchState.startSearch) {
            const value = e.node.eventKey as Key;
            currentValue = difference(currentValue, getChildrenKeys(value));
            if (e.checked) {
              currentValue.push(value);
            }
            state.checkedKeys = currentValue;
          } else {
            state.checkedKeys = v;
          }

          const rawVal = toRaw(state.checkedKeys);
          emit('update:value', rawVal);
          emit('check', rawVal, e);
        },
        onRightClick: handleRightClick,
      };
      return omit(propsData, 'treeData', 'class');
    });

    const getTreeData = computed((): TreeItem[] =>
      searchState.startSearch ? searchState.searchData : unref(treeDataRef),
    );

    const getNotFound = computed((): boolean => {
      return !getTreeData.value || getTreeData.value.length === 0;
    });

    const {
      deleteNodeByKey,
      insertNodeByKey,
      insertNodesByKey,
      filterByLevel,
      updateNodeByKey,
      getAllKeys,
      getChildrenKeys,
      getEnabledKeys,
      getSelectedNode,
    } = useTree(treeDataRef, getFieldNames);

    function getIcon(params: Recordable, icon?: string) {
      if (!icon && props.renderIcon && isFunction(props.renderIcon)) {
        return props.renderIcon(params);
      }
      return icon;
    }

    async function handleRightClick({ event, node }: Recordable) {
      const { rightMenuList: menuList = [], beforeRightClick } = props;
      const contextMenuOptions: CreateContextOptions = { event, items: [] };

      if (beforeRightClick && isFunction(beforeRightClick)) {
        const result = await beforeRightClick(node, event);
        if (Array.isArray(result)) {
          contextMenuOptions.items = result;
        } else {
          Object.assign(contextMenuOptions, result);
        }
      } else {
        contextMenuOptions.items = menuList;
      }
      if (!contextMenuOptions.items?.length) return;
      contextMenuOptions.items = contextMenuOptions.items.filter(
        (item) => !item.hidden,
      );
      createContextMenu && createContextMenu(contextMenuOptions);
    }

    function setExpandedKeys(keys: Key[]) {
      state.expandedKeys = keys;
    }

    function getExpandedKeys() {
      return state.expandedKeys;
    }
    function setSelectedKeys(keys: Key[]) {
      state.selectedKeys = keys;
    }

    function getSelectedKeys() {
      return state.selectedKeys;
    }

    function getCheckStrictly() {
      return state.checkStrictly;
    }

    function setCheckedKeys(keys: CheckKeys) {
      state.checkedKeys = keys;
    }

    function getCheckedKeys() {
      return state.checkedKeys;
    }

    function checkAll(checkAll: boolean) {
      if (getCheckStrictly()) {
        state.checkedKeys = checkAll
          ? { checked: getEnabledKeys(), halfChecked: [] }
          : { checked: [], halfChecked: [] };
      } else {
        state.checkedKeys = checkAll ? getEnabledKeys() : ([] as Key[]);
      }
    }

    function expandAll(expandAll: boolean) {
      state.expandedKeys = expandAll ? getAllKeys() : ([] as Key[]);
    }

    function onStrictlyChange(strictly: boolean) {
      state.checkStrictly = strictly;
      checkAll(false);
    }

    watch(
      () => props.searchValue,
      (val) => {
        if (val !== searchState.searchText) {
          searchState.searchText = val;
        }
      },
      {
        immediate: true,
      },
    );

    watch(
      () => props.treeData,
      (val) => {
        if (val) {
          handleSearch(searchState.searchText);
        }
      },
    );

    function handleSearch(searchValue: string) {
      if (searchValue !== searchState.searchText)
        searchState.searchText = searchValue;
      emit('update:searchValue', searchValue);
      if (!searchValue) {
        searchState.startSearch = false;
        return;
      }
      const {
        filterFn,
        checkable,
        expandOnSearch,
        checkOnSearch,
        selectedOnSearch,
      } = unref(props);
      searchState.startSearch = true;
      const { title: titleField, key: keyField } = unref(getFieldNames);

      const matchedKeys: string[] = [];
      searchState.searchData = filter(
        unref(treeDataRef),
        (node) => {
          const result = filterFn
            ? filterFn(searchValue, node, unref(getFieldNames))
            : (node[titleField]?.includes(searchValue) ?? false);
          if (result) {
            matchedKeys.push(node[keyField]);
          }
          return result;
        },
        unref(getFieldNames),
      );

      if (expandOnSearch) {
        const expandKeys = treeToList(searchState.searchData).map((val) => {
          return val[keyField];
        });
        if (expandKeys && expandKeys.length > 0) {
          setExpandedKeys(expandKeys);
        }
      }

      if (checkOnSearch && checkable && matchedKeys.length > 0) {
        setCheckedKeys(matchedKeys);
      }

      if (selectedOnSearch && matchedKeys.length > 0) {
        setSelectedKeys(matchedKeys);
      }
    }

    function handleClickNode(key: string, children: TreeItem[]) {
      if (!props.clickRowToExpand || !children || children.length === 0) return;
      if (state.expandedKeys.includes(key)) {
        const keys = [...state.expandedKeys];
        const index = keys.indexOf(key);
        if (index !== -1) {
          keys.splice(index, 1);
        }
        setExpandedKeys(keys);
      } else {
        setExpandedKeys([...state.expandedKeys, key]);
      }
    }

    watchEffect(() => {
      treeDataRef.value = props.treeData as TreeItem[];
    });

    onMounted(() => {
      const level = Number.parseInt(props.defaultExpandLevel);
      if (level > 0) {
        state.expandedKeys = filterByLevel(level);
      } else if (props.defaultExpandAll) {
        expandAll(true);
      }
    });

    watchEffect(() => {
      state.expandedKeys = props.expandedKeys;
    });

    watchEffect(() => {
      state.selectedKeys = props.selectedKeys;
    });

    watchEffect(() => {
      state.checkedKeys = props.checkedKeys;
    });

    watch(
      () => props.value,
      () => {
        state.checkedKeys = toRaw(props.value || []);
      },
      { immediate: true },
    );

    watch(
      () => state.checkedKeys,
      () => {
        const v = toRaw(state.checkedKeys);
        emit('update:value', v);
        emit('change', v);
      },
    );

    watchEffect(() => {
      state.checkStrictly = props.checkStrictly;
    });

    const instance: TreeActionType = {
      setExpandedKeys,
      getExpandedKeys,
      setSelectedKeys,
      getSelectedKeys,
      getCheckStrictly,
      setCheckedKeys,
      getCheckedKeys,
      insertNodeByKey,
      insertNodesByKey,
      deleteNodeByKey,
      updateNodeByKey,
      getSelectedNode,
      checkAll,
      expandAll,
      filterByLevel: (level: number) => {
        state.expandedKeys = filterByLevel(level);
      },
      setSearchValue: (value: string) => {
        handleSearch(value);
      },
      getSearchValue: () => {
        return searchState.searchText;
      },
    };

    const { isPermission } = useAccess();

    function renderAction(node: TreeItem) {
      const { actionList } = props;
      if (!actionList || actionList.length === 0) return;
      return actionList.map((item: TreeActionItem, index: number) => {
        let nodeShow = true;
        if (isFunction(item.show)) {
          nodeShow = item.show?.(node);
        } else if (isBoolean(item.show)) {
          nodeShow = item.show;
        }

        if (!nodeShow) return null;

        if (!isPermission(item.auth, true, item.authMode)) {
          return null;
        }

        return (
          <span class={e('action')} key={index}>
            {item.render(node)}
          </span>
        );
      });
    }

    const treeData = computed(() => {
      const data = cloneDeep(getTreeData.value);
      eachTree(data, (item: TreeItem, _parent: TreeItem) => {
        const searchText = searchState.searchText;
        const { highlight } = unref(props);
        const {
          title: titleField,
          key: keyField,
          children: childrenField,
        } = unref(getFieldNames);

        const icon = getIcon(item, item.icon);
        const title = get(item, titleField);

        const searchIdx = searchText ? title.indexOf(searchText) : -1;
        const isHighlight =
          searchState.startSearch &&
          !isEmpty(searchText) &&
          highlight &&
          searchIdx !== -1;
        const highlightStyle = `color: ${isBoolean(highlight) ? '#f50' : highlight}`;

        const titleDom = isHighlight ? (
          <span
            class={unref(getBindValues)?.blockNode ? `${e('content')}` : ''}
          >
            <span>{title.slice(0, Math.max(0, searchIdx))}</span>
            <span style={highlightStyle}>{searchText}</span>
            <span>
              {title.slice(searchIdx + (searchText as string).length)}
            </span>
          </span>
        ) : (
          title
        );

        // item[titleField] = (
        //   <span
        //     class={`${e('title')} pl-2`}
        //     onClick={handleClickNode.bind(
        //       null,
        //       item[keyField],
        //       item[childrenField],
        //     )}
        //   >
        //     {
        //       <>
        //         {icon && <Icon class="mr-2" icon={icon} />}
        //         {slots?.titleBefore && getSlot(slots, 'titleBefore', item)}
        //         {titleDom}
        //         {slots?.titleAfter && getSlot(slots, 'titleAfter', item)}
        //         <span class={e('actions')}>{renderAction(item)}</span>
        //       </>
        //     }
        //   </span>
        // );
        // return item;
        item[titleField] = (
          <span
            class={`${e('title')} pl-2`}
            onClick={handleClickNode.bind(
              null,
              item[keyField],
              item[childrenField],
            )}
          >
            {item?.slots?.title ? (
              getSlot(slots, 'title', item)
            ) : (
              <>
                {icon && <Icon icon={icon} />}
                {item?.slots?.titleBefore &&
                  getSlot(slots, item?.slots?.titleBefore, item)}
                {titleDom}
                {item?.slots?.titleAfter &&
                  getSlot(slots, item?.slots?.titleAfter, item)}
                <span class={e('actions')}>{renderAction(item)}</span>
              </>
            )}
          </span>
        );
        delete item?.slots;
        return item;
      });
      return data;
    });

    expose(instance);

    return () => {
      const {
        title,
        helpMessage,
        toolbar,
        toolbarStrictly,
        search,
        checkable,
      } = props;
      const showTitle = title || toolbar || search || slots.headerTitle;
      const scrollStyle: CSSProperties = { height: 'calc(100% - 38px)' };
      return (
        <div class={[b(), 'h-full', attrs.class]}>
          {showTitle && (
            <TreeHeader
              checkable={checkable}
              checkAll={checkAll}
              checkStrictly={state.checkStrictly}
              expandAll={expandAll}
              helpMessage={helpMessage}
              onSearch={handleSearch}
              onStrictlyChange={onStrictlyChange}
              search={search}
              searchText={searchState.searchText}
              title={title}
              toolbar={toolbar}
              toolbarStrictly={toolbarStrictly}
            >
              {extendSlots(slots)}
            </TreeHeader>
          )}
          <Spin
            spinning={unref(props.loading)}
            tip="加载中..."
            wrapperClassName={unref(props.treeWrapperClassName)}
          >
            <ScrollContainer style={scrollStyle} v-show={!unref(getNotFound)}>
              <Tree
                {...unref(getBindValues)}
                showIcon={false}
                treeData={treeData.value}
              ></Tree>
            </ScrollContainer>
            <Empty
              class="!mt-4"
              image={Empty.PRESENTED_IMAGE_SIMPLE}
              v-show={unref(getNotFound)}
            />
          </Spin>
        </div>
      );
    };
  },
});
</script>
